package com.zhuxulin.midi.util;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;

import android.os.Process;

import com.zhuxulin.midi.MidiFile;
import com.zhuxulin.midi.MidiTrack;
import com.zhuxulin.midi.event.MidiEvent;
import com.zhuxulin.midi.event.meta.Tempo;
import com.zhuxulin.midi.event.meta.TimeSignature;

public class MidiProcessor {

	private static final int PROCESS_RATE_MS = 8;

	private HashMap<Class<? extends MidiEvent>, ArrayList<MidiEventListener>> mEventsToListeners;
	private HashMap<MidiEventListener, ArrayList<Class<? extends MidiEvent>>> mListenersToEvents;

	private MidiFile mMidiFile;
	private boolean mRunning;
	private double mTicksElapsed;
	private long mMsElapsed;

	boolean finished = false;

	private int mMPQN;
	private int mPPQ;

	private MetronomeTick mMetronome;
	private MidiTrackEventQueue[] mEventQueues;

	public MidiProcessor(MidiFile input) {

		// mMidiFile = input;
		//
		// mMPQN = Tempo.DEFAULT_MPQN;
		// mPPQ = mMidiFile.getResolution();
		//
		// mEventsToListeners = new HashMap<Class<? extends MidiEvent>,
		// ArrayList<MidiEventListener>>();
		// mListenersToEvents = new HashMap<MidiEventListener, ArrayList<Class<?
		// extends MidiEvent>>>();
		//
		// mMetronome = new MetronomeTick(new TimeSignature(), mPPQ);
		//
		// this.reset();
		init(input);
	}

	public void setMidiFile(MidiFile file) {
		// this.mMidiFile=file;
		init(file);
	}

	private void init(MidiFile input) {
		mMidiFile = input;

		mMPQN = Tempo.DEFAULT_MPQN;
		mPPQ = mMidiFile.getResolution();

		mEventsToListeners = new HashMap<Class<? extends MidiEvent>, ArrayList<MidiEventListener>>();
		mListenersToEvents = new HashMap<MidiEventListener, ArrayList<Class<? extends MidiEvent>>>();

		mMetronome = new MetronomeTick(new TimeSignature(), mPPQ);

		this.reset();
	}

	public synchronized void start() {
		if (finished) {
			this.reset();
		}
		if (mRunning)
			return;

		mRunning = true;
		new Thread(new Runnable() {
			public void run() {
				Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO);
				process();
			}
		}).start();
	}

	public void stop() {
		// mRunning = false;
		this.reset();
	}

	public void pause() {
		mRunning = !mRunning;
	}

	private void reset() {

		mRunning = false;
		mTicksElapsed = 0;
		mMsElapsed = 0;

		mMetronome.setTimeSignature(new TimeSignature());

		ArrayList<MidiTrack> tracks = mMidiFile.getTracks();

		if (mEventQueues == null) {
			mEventQueues = new MidiTrackEventQueue[tracks.size()];
		}

		for (int i = 0; i < tracks.size(); i++) {
			mEventQueues[i] = new MidiTrackEventQueue(tracks.get(i));
		}
	}

	public boolean isStarted() {
		return mTicksElapsed > 0;
	}

	public boolean isRunning() {
		return mRunning;
	}

	protected void onStart(boolean fromBeginning) {

		Iterator<MidiEventListener> it = mListenersToEvents.keySet().iterator();

		while (it.hasNext()) {

			MidiEventListener mel = it.next();
			mel.onStart(fromBeginning);
		}
	}

	protected void onStop(boolean finished) {
		Iterator<MidiEventListener> it = mListenersToEvents.keySet().iterator();
		while (it.hasNext()) {
			MidiEventListener mel = it.next();
			mel.onStop(finished);
		}
	}

	public void registerEventListener(MidiEventListener mel,
			Class<? extends MidiEvent> event) {
		ArrayList<MidiEventListener> listeners = mEventsToListeners.get(event);
		if (listeners == null) {
			listeners = new ArrayList<MidiEventListener>();
			listeners.add(mel);
			mEventsToListeners.put(event, listeners);
		} else {
			listeners.add(mel);
		}
		ArrayList<Class<? extends MidiEvent>> events = mListenersToEvents
				.get(mel);
		if (events == null) {
			events = new ArrayList<Class<? extends MidiEvent>>();
			events.add(event);
			mListenersToEvents.put(mel, events);
		} else {
			events.add(event);
		}
	}

	public void unregisterEventListener(MidiEventListener mel) {

		ArrayList<Class<? extends MidiEvent>> events = mListenersToEvents
				.get(mel);
		if (events == null) {
			return;
		}

		for (Class<? extends MidiEvent> event : events) {

			ArrayList<MidiEventListener> listeners = mEventsToListeners
					.get(event);
			listeners.remove(mel);
		}

		mListenersToEvents.remove(mel);
	}

	public void unregisterEventListener(MidiEventListener mel,
			Class<? extends MidiEvent> event) {

		ArrayList<MidiEventListener> listeners = mEventsToListeners.get(event);
		if (listeners != null) {
			listeners.remove(mel);
		}

		ArrayList<Class<? extends MidiEvent>> events = mListenersToEvents
				.get(mel);
		if (events != null) {
			events.remove(event);
		}
	}

	public void unregisterAllEventListeners() {
		mEventsToListeners.clear();
		mListenersToEvents.clear();
	}

	protected void dispatch(MidiEvent event) {

		// Tempo and Time Signature events are always needed by the processor
		if (event.getClass().equals(Tempo.class)) {
			mMPQN = ((Tempo) event).getMpqn();
			System.out.println("getTempo" + mMPQN);
		} else if (event.getClass().equals(TimeSignature.class)) {

			boolean shouldDispatch = mMetronome.getBeatNumber() != 1;
			mMetronome.setTimeSignature((TimeSignature) event);

			if (shouldDispatch) {
				dispatch(mMetronome);
			}
		}

//		this.sendOnEventForClass(event, event.getClass());
		this.sendOnEventForClass(event, MidiEvent.class);
	}
	private void sendEvents(ArrayList<MidiEvent> events) {

		for (MidiEvent event : events) {

			// Tempo and Time Signature events are always needed by the
			// processor
			if (event.getClass().equals(Tempo.class)) {
				mMPQN = ((Tempo) event).getMpqn();
				System.out.println("getTempo" + mMPQN);
			} else if (event.getClass().equals(TimeSignature.class)) {

				boolean shouldDispatch = mMetronome.getBeatNumber() != 1;
				mMetronome.setTimeSignature((TimeSignature) event);

				if (shouldDispatch) {
					dispatch(mMetronome);
				}
			}
		}
//		this.sendOnEventForClass(event, event.getClass());
//		this.sendOnEventForClass(event, MidiEvent.class);
		this.sendEventsForClass(events, MidiEvent.class);
	}

	private void sendEventsForClass(ArrayList<MidiEvent> events,
			Class<MidiEvent> eventClass) {

		ArrayList<MidiEventListener> listeners = mEventsToListeners
				.get(eventClass);

		if (listeners == null) {
			return;
		}

		for (MidiEventListener mel : listeners) {
			mel.onEvents(events, mMsElapsed);
		}
	}

	private void sendOnEventForClass(MidiEvent event,
			Class<? extends MidiEvent> eventClass) {

		ArrayList<MidiEventListener> listeners = mEventsToListeners
				.get(eventClass);

		if (listeners == null) {
			return;
		}

		for (MidiEventListener mel : listeners) {
			mel.onEvent(event, mMsElapsed);
		}
	}

	private void process() {

		onStart(mTicksElapsed < 1);

		long lastMs = System.currentTimeMillis();

		while (mRunning) {

			long now = System.currentTimeMillis();
			long msElapsed = now - lastMs;
			while(now - lastMs<PROCESS_RATE_MS){
				now = System.currentTimeMillis();
				msElapsed = now - lastMs;
			}
			/**
			if (msElapsed < PROCESS_RATE_MS) {
				try {
					Thread.sleep(PROCESS_RATE_MS - msElapsed);
				} catch (Exception e) {
				}
				continue;
			}*/

			double ticksElapsed = MidiUtil.msToTicks(msElapsed, mMPQN, mPPQ);

			if (ticksElapsed < 1) {
				continue;
			}

			if (mMetronome.update(ticksElapsed)) {
				dispatch(mMetronome);
			}

			lastMs = now;
			mMsElapsed += msElapsed;
			mTicksElapsed += ticksElapsed;

			boolean more = false;
			for (int i = 0; i < mEventQueues.length; i++) {

				MidiTrackEventQueue queue = mEventQueues[i];
				if (!queue.hasMoreEvents()) {
					continue;
				}

				ArrayList<MidiEvent> events = queue
						.getNextEventsUpToTick(mTicksElapsed);
				/**
				for (MidiEvent event : events) {
					this.dispatch(event);
				}
				*/
				this.sendEvents(events);
				if (queue.hasMoreEvents()) {
					more = true;
				}
			}

			if (!more) {
				finished = true;
				break;
			}
		}

		mRunning = false;
		onStop(finished);
	}

	private class MidiTrackEventQueue {

		private MidiTrack mTrack;
		private Iterator<MidiEvent> mIterator;
		private ArrayList<MidiEvent> mEventsToDispatch;
		private MidiEvent mNext;

		public MidiTrackEventQueue(MidiTrack track) {

			mTrack = track;

			mIterator = mTrack.getEvents().iterator();
			mEventsToDispatch = new ArrayList<MidiEvent>();

			if (mIterator.hasNext()) {
				mNext = mIterator.next();
			}
		}

		/**
		 * Find the next tick that tick less than the
		 * parameter!(mNext.getTick())
		 * 
		 * @param tick
		 * @return
		 */
		public ArrayList<MidiEvent> getNextEventsUpToTick(double tick) {

			mEventsToDispatch.clear();

			while (mNext != null) {

				if (mNext.getTick() <= tick) {
					mEventsToDispatch.add(mNext);

					if (mIterator.hasNext()) {
						mNext = mIterator.next();
					} else {
						mNext = null;
					}
				} else {
					break;
				}
			}

			return mEventsToDispatch;
		}

		public boolean hasMoreEvents() {
			return mNext != null;
		}
	}
}
